import 'dart:convert';
import 'dart:io';
import 'dart:math';

import 'package:built_collection/built_collection.dart';
import 'package:compute/compute.dart';
import 'package:dartz/dartz.dart';
import 'package:encrypt/encrypt.dart';
import 'package:equatable/equatable.dart';
import 'package:mobile_app.core/core/domain/config/config.dart';
import 'package:mobile_app.core/core/domain/entities/learning_content/collection_list_item/collection_list_item.dart';
import 'package:mobile_app.core/core/domain/entities/learning_info/learning_info.dart';
import 'package:mobile_app.core/core/domain/entities/learning_content/lexical_item/lexical_item.dart';
import 'package:mobile_app.home.menu/menu_page/domain/entities/backup.dart';
import 'package:mobile_app.home.menu/menu_page/domain/entities/backup_collection_meta.dart';
import 'package:mobile_app.home.menu/menu_page/domain/entities/backup_custom_collection_list_item_data.dart';
import 'package:mobile_app.home.menu/menu_page/domain/entities/backup_custom_lexical_item_data.dart';
import 'package:mobile_app.home.menu/menu_page/domain/entities/backup_custom_lexical_item_use_case.dart';
import 'package:mobile_app.home.menu/menu_page/domain/entities/backup_lexical_item_meta.dart';
import 'package:mobile_app.home.menu/menu_page/domain/repositories/backup_repository/repository.dart';

part 'dtos/backup_dto.dart';

part 'dtos/backup_file_content_dto.dart';

part 'dtos/collection_meta_dto.dart';

part 'dtos/collection_sorting_dto.dart';

part 'dtos/custom_collection_data_dto.dart';

part 'dtos/custom_collection_folder_data_dto.dart';

part 'dtos/custom_collection_list_item_data_dto_factory.dart';

part 'dtos/custom_collection_list_item_dto.dart';

part 'dtos/custom_lexical_item_dto.dart';

part 'dtos/custom_lexical_item_use_case_dto.dart';

part 'dtos/learning_event_dto.dart';

part 'dtos/learning_info_dto.dart';

part 'dtos/learning_interval_dto.dart';

part 'dtos/learning_lexical_item_progress_dto.dart';

part 'dtos/learning_session_dto.dart';

part 'dtos/learning_stage_dto.dart';

part 'dtos/learning_stage_dto_factory.dart';

part 'dtos/lexical_item_meta_dto.dart';

part 'dtos/lexical_item_progress_dto.dart';

part 'dtos/lexical_item_progress_factory_dto.dart';

part 'dtos/previously_known_lexical_item_progress_dto.dart';

part 'dtos/repeatable_learning_stage_dto.dart';

part 'dtos/suspended_learning_stage_dto.dart';

part 'dtos/time_dto.dart';

part 'migrations/migrate_backup_json_from_first_version_to_second_one.dart';

part 'migrations/migrate_backup_json_from_second_version_to_third_one.dart';

class BackupRepositoryImpl implements BackupRepository {
  BackupRepositoryImpl({
    required Encrypter encrypter,
    required IV iv,
  })  : _encrypter = encrypter,
        _iv = iv;

  final Encrypter _encrypter;
  final IV _iv;

  static const _version = 3;

  static const _kBackupFileTypeValue = 'backup';

  @override
  Future<List<int>> buildBackupFileContentBytes({
    required Backup backup,
  }) async {
    final backupDto = _BackupDto.fromEntity(backup);
    final backupJson = backupDto.toJson();
    final backupContentString = await compute(_jsonEncode, backupJson);
    final backupContentEncryptedString = _encrypt(backupContentString);

    final backupFileContentDto = _BackupFileContentDto._internal(
      fileType: _kBackupFileTypeValue,
      version: _version,
      languageCode: Config().nativeLanguageCode,
      data: backupContentEncryptedString,
    );

    final backupFileContentJson = backupFileContentDto.toJson();
    final backupFileContentString = await compute(_jsonEncode, backupFileContentJson);
    final backupFileContentBase64Bytes =
        utf8.encode(base64.encode(utf8.encode(backupFileContentString)));

    return backupFileContentBase64Bytes;
  }

  @override
  Future<Either<BackupGettingFailure, Backup>> getBackup({
    required String filePath,
  }) async {
    try {
      final file = File(filePath);

      final backupFileContentBase64String = file.readAsStringSync();
      final backupFileContentString = utf8.decode(base64.decode(backupFileContentBase64String));
      final backupFileContentJson = await compute(_jsonDecode, backupFileContentString);

      final backupFileContentDto = _BackupFileContentDto.fromJson(backupFileContentJson);

      final fileType = backupFileContentDto.fileType;
      final version = backupFileContentDto.version;
      final languageCode = backupFileContentDto.languageCode;

      if (fileType != _kBackupFileTypeValue) {
        throw const FormatException();
      }

      if (version < 1 || version > _version) {
        throw const FormatException();
      }

      if (languageCode != 'ru') {
        throw const FormatException();
      }

      final backupContentEncryptedString = backupFileContentDto.data;
      final backupContentString = _decrypt(backupContentEncryptedString);
      final backupJson = await compute(_jsonDecode, backupContentString);

      var migratedBackupContent = BuiltMap.of(backupJson);
      var migratedContentVersion = version;

      if (migratedContentVersion == 1) {
        migratedBackupContent = _migrateBackupContentFromFirstVersionToSecondOne(
          backupContent: migratedBackupContent,
        );

        migratedContentVersion = 2;
      }

      if (migratedContentVersion == 2) {
        migratedBackupContent = _migrateBackupContentFromSecondVersionToThirdOne(
          backupContent: migratedBackupContent,
        );

        migratedContentVersion = 3;
      }

      final migratedBackupJson = migratedBackupContent.toMap();
      final backupDto = _BackupDto.fromJson(migratedBackupJson);
      final backup = backupDto.toEntity();

      return right(backup);
    } catch (_) {
      const failure = InvalidFormatBackupGettingFailure();
      return left(failure);
    }
  }

  String _encrypt(String input) {
    return _encrypter.encrypt(input, iv: _iv).base64;
  }

  String _decrypt(String input) {
    return _encrypter.decrypt(Encrypted.from64(input), iv: _iv);
  }
}

String _jsonEncode(Map<String, dynamic> json) {
  return jsonEncode(json);
}

Map<String, dynamic> _jsonDecode(String data) {
  return jsonDecode(data);
}
